package org.andengine.entity.text;

import android.opengl.GLES20;

import org.andengine.engine.camera.Camera;
import org.andengine.entity.shape.RectangularShape;
import org.andengine.entity.text.exception.OutOfCharactersException;
import org.andengine.entity.text.vbo.HighPerformanceTextVertexBufferObject;
import org.andengine.entity.text.vbo.ITextVertexBufferObject;
import org.andengine.opengl.font.FontUtils;
import org.andengine.opengl.font.IFont;
import org.andengine.opengl.shader.PositionColorTextureCoordinatesShaderProgram;
import org.andengine.opengl.shader.ShaderProgram;
import org.andengine.opengl.shader.constants.ShaderProgramConstants;
import org.andengine.opengl.util.GLState;
import org.andengine.opengl.vbo.DrawType;
import org.andengine.opengl.vbo.VertexBufferObjectManager;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributes;
import org.andengine.opengl.vbo.attribute.VertexBufferObjectAttributesBuilder;
import org.andengine.util.HorizontalAlign;
import org.andengine.util.adt.DataConstants;
import org.andengine.util.adt.list.FloatArrayList;
import org.andengine.util.adt.list.IFloatList;

import java.util.ArrayList;

/**
 * TODO Try Degenerate Triangles?
 * <p/>
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 10:54:59 - 03.04.2010
 */
public class Text extends RectangularShape {
    // ===========================================================
    // Constants
    // ===========================================================

    public static final float LEADING_DEFAULT = 0;

    public static final int VERTEX_INDEX_X = 0;
    public static final int VERTEX_INDEX_Y = Text.VERTEX_INDEX_X + 1;
    public static final int COLOR_INDEX = Text.VERTEX_INDEX_Y + 1;
    public static final int TEXTURECOORDINATES_INDEX_U = Text.COLOR_INDEX + 1;
    public static final int TEXTURECOORDINATES_INDEX_V = Text.TEXTURECOORDINATES_INDEX_U + 1;

    public static final int VERTEX_SIZE = 2 + 1 + 2;
    public static final int VERTICES_PER_LETTER = 6;
    public static final int LETTER_SIZE = Text.VERTEX_SIZE * Text.VERTICES_PER_LETTER;
    public static final int VERTEX_STRIDE = Text.VERTEX_SIZE * DataConstants.BYTES_PER_FLOAT;

    public static final VertexBufferObjectAttributes VERTEXBUFFEROBJECTATTRIBUTES_DEFAULT = new VertexBufferObjectAttributesBuilder(3)
            .add(ShaderProgramConstants.ATTRIBUTE_POSITION_LOCATION, ShaderProgramConstants.ATTRIBUTE_POSITION, 2, GLES20.GL_FLOAT, false)
            .add(ShaderProgramConstants.ATTRIBUTE_COLOR_LOCATION, ShaderProgramConstants.ATTRIBUTE_COLOR, 4, GLES20.GL_UNSIGNED_BYTE, true)
            .add(ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES_LOCATION, ShaderProgramConstants.ATTRIBUTE_TEXTURECOORDINATES, 2, GLES20.GL_FLOAT, false)
            .build();

    // ===========================================================
    // Fields
    // ===========================================================

    protected final IFont mFont;

    protected float mLineWidthMaximum;
    protected float mLineAlignmentWidth;

    protected TextOptions mTextOptions;
    protected final int mCharactersMaximum;
    protected int mCharactersToDraw;
    protected int mVertexCountToDraw;
    protected final int mVertexCount;

    protected final ITextVertexBufferObject mTextVertexBufferObject;

    protected CharSequence mText;
    protected ArrayList<CharSequence> mLines = new ArrayList<CharSequence>(1);
    protected IFloatList mLineWidths = new FloatArrayList(1);

    // ===========================================================
    // Constructors
    // ===========================================================

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final VertexBufferObjectManager pVertexBufferObjectManager) {
        this(pX, pY, pFont, pText, pVertexBufferObjectManager, DrawType.STATIC);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final VertexBufferObjectManager pVertexBufferObjectManager, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, pVertexBufferObjectManager, DrawType.STATIC, pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType) {
        this(pX, pY, pFont, pText, new TextOptions(), pVertexBufferObjectManager, pDrawType);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, new TextOptions(), pVertexBufferObjectManager, pDrawType, pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager) {
        this(pX, pY, pFont, pText, pTextOptions, pVertexBufferObjectManager, DrawType.STATIC);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, pTextOptions, pVertexBufferObjectManager, DrawType.STATIC, pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType) {
        this(pX, pY, pFont, pText, pText.length(), pTextOptions, pVertexBufferObjectManager, pDrawType);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, pText.length(), pTextOptions, pVertexBufferObjectManager, pDrawType, pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final VertexBufferObjectManager pVertexBufferObjectManager) {
        this(pX, pY, pFont, pText, pCharactersMaximum, pVertexBufferObjectManager, DrawType.STATIC);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final VertexBufferObjectManager pVertexBufferObjectManager, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, pCharactersMaximum, pVertexBufferObjectManager, DrawType.STATIC, pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType) {
        this(pX, pY, pFont, pText, pCharactersMaximum, new TextOptions(), pVertexBufferObjectManager, pDrawType);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, pCharactersMaximum, new TextOptions(), pVertexBufferObjectManager, pDrawType, pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager) {
        this(pX, pY, pFont, pText, pCharactersMaximum, pTextOptions, pVertexBufferObjectManager, DrawType.STATIC);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType) {
        this(pX, pY, pFont, pText, pCharactersMaximum, pTextOptions, new HighPerformanceTextVertexBufferObject(pVertexBufferObjectManager, Text.LETTER_SIZE * pCharactersMaximum, pDrawType, true, Text.VERTEXBUFFEROBJECTATTRIBUTES_DEFAULT));
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final TextOptions pTextOptions, final VertexBufferObjectManager pVertexBufferObjectManager, final DrawType pDrawType, final ShaderProgram pShaderProgram) {
        this(pX, pY, pFont, pText, pCharactersMaximum, pTextOptions, new HighPerformanceTextVertexBufferObject(pVertexBufferObjectManager, Text.LETTER_SIZE * pCharactersMaximum, pDrawType, true, Text.VERTEXBUFFEROBJECTATTRIBUTES_DEFAULT), pShaderProgram);
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final TextOptions pTextOptions, final ITextVertexBufferObject pTextVertexBufferObject) {
        this(pX, pY, pFont, pText, pCharactersMaximum, pTextOptions, pTextVertexBufferObject, PositionColorTextureCoordinatesShaderProgram.getInstance());
    }

    public Text(final float pX, final float pY, final IFont pFont, final CharSequence pText, final int pCharactersMaximum, final TextOptions pTextOptions, final ITextVertexBufferObject pTextVertexBufferObject, final ShaderProgram pShaderProgram) {
        super(pX, pY, 0, 0, pShaderProgram);

        this.mFont = pFont;
        this.mTextOptions = pTextOptions;
        this.mCharactersMaximum = pCharactersMaximum;
        this.mVertexCount = Text.VERTICES_PER_LETTER * this.mCharactersMaximum;
        this.mTextVertexBufferObject = pTextVertexBufferObject;

        this.onUpdateColor();
        this.setText(pText);

        this.setBlendingEnabled(true);
        this.initBlendFunction(this.mFont.getTexture());
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public IFont getFont() {
        return this.mFont;
    }

    public int getCharactersMaximum() {
        return this.mCharactersMaximum;
    }

    public CharSequence getText() {
        return this.mText;
    }

    /**
     * @param pText
     * @throws OutOfCharactersException leaves this {@link Text} object in an undefined state, until {@link Text#setText(CharSequence)} is called again and no {@link OutOfCharactersException} is thrown.
     */
    public void setText(final CharSequence pText) throws OutOfCharactersException {
        this.mText = pText;
        final IFont font = this.mFont;

        this.mLines.clear();
        this.mLineWidths.clear();

        if (this.mTextOptions.mAutoWrap == AutoWrap.NONE) {
            this.mLines = FontUtils.splitLines(this.mText, this.mLines); // TODO Add whitespace-trimming.
        } else {
            this.mLines = FontUtils.splitLines(this.mFont, this.mText, this.mLines, this.mTextOptions.mAutoWrap, this.mTextOptions.mAutoWrapWidth);
        }

        final int lineCount = this.mLines.size();
        float maximumLineWidth = 0;
        for (int i = 0; i < lineCount; i++) {
            final float lineWidth = FontUtils.measureText(font, this.mLines.get(i));
            maximumLineWidth = Math.max(maximumLineWidth, lineWidth);

            this.mLineWidths.add(lineWidth);
        }
        this.mLineWidthMaximum = maximumLineWidth;

        if (this.mTextOptions.mAutoWrap == AutoWrap.NONE) {
            this.mLineAlignmentWidth = this.mLineWidthMaximum;
        } else {
            this.mLineAlignmentWidth = this.mTextOptions.mAutoWrapWidth;
        }

        super.mWidth = this.mLineAlignmentWidth;
        super.mHeight = lineCount * font.getLineHeight() + (lineCount - 1) * this.mTextOptions.mLeading;

        this.mRotationCenterX = super.mWidth * 0.5f;
        this.mRotationCenterY = super.mHeight * 0.5f;

        this.mScaleCenterX = this.mRotationCenterX;
        this.mScaleCenterY = this.mRotationCenterY;

        this.onUpdateVertices();
    }

    public ArrayList<CharSequence> getLines() {
        return this.mLines;
    }

    public IFloatList getLineWidths() {
        return this.mLineWidths;
    }

    public float getLineAlignmentWidth() {
        return this.mLineAlignmentWidth;
    }

    public float getLineWidthMaximum() {
        return this.mLineWidthMaximum;
    }

    public float getLeading() {
        return this.mTextOptions.mLeading;
    }

    public void setLeading(final float pLeading) {
        this.mTextOptions.mLeading = pLeading;

        this.invalidateText();
    }

    public HorizontalAlign getHorizontalAlign() {
        return this.mTextOptions.mHorizontalAlign;
    }

    public void setHorizontalAlign(final HorizontalAlign pHorizontalAlign) {
        this.mTextOptions.mHorizontalAlign = pHorizontalAlign;

        this.invalidateText();
    }

    public AutoWrap getAutoWrap() {
        return this.mTextOptions.mAutoWrap;
    }

    public void setAutoWrap(final AutoWrap pAutoWrap) {
        this.mTextOptions.mAutoWrap = pAutoWrap;

        this.invalidateText();
    }

    public float getAutoWrapWidth() {
        return this.mTextOptions.mAutoWrapWidth;
    }

    public void setAutoWrapWidth(final float pAutoWrapWidth) {
        this.mTextOptions.mAutoWrapWidth = pAutoWrapWidth;

        this.invalidateText();
    }

    public TextOptions getTextOptions() {
        return this.mTextOptions;
    }

    public void setTextOptions(final TextOptions pTextOptions) {
        this.mTextOptions = pTextOptions;
    }

    public void setCharactersToDraw(final int pCharactersToDraw) {
        if (pCharactersToDraw > this.mCharactersMaximum) {
            throw new OutOfCharactersException("Characters: maximum: '" + this.mCharactersMaximum + "' required: '" + pCharactersToDraw + "'.");
        }
        this.mCharactersToDraw = pCharactersToDraw;
        this.mVertexCountToDraw = pCharactersToDraw * Text.VERTICES_PER_LETTER;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public ITextVertexBufferObject getVertexBufferObject() {
        return this.mTextVertexBufferObject;
    }

    @Override
    protected void preDraw(final GLState pGLState, final Camera pCamera) {
        super.preDraw(pGLState, pCamera);

        this.mFont.getTexture().bind(pGLState);

        this.mTextVertexBufferObject.bind(pGLState, this.mShaderProgram);
    }

    @Override
    protected void draw(final GLState pGLState, final Camera pCamera) {
        this.mTextVertexBufferObject.draw(GLES20.GL_TRIANGLES, this.mVertexCountToDraw);
    }

    @Override
    protected void postDraw(final GLState pGLState, final Camera pCamera) {
        this.mTextVertexBufferObject.unbind(pGLState, this.mShaderProgram);

        super.postDraw(pGLState, pCamera);
    }

    @Override
    protected void onUpdateColor() {
        this.mTextVertexBufferObject.onUpdateColor(this);
    }

    @Override
    protected void onUpdateVertices() {
        this.mTextVertexBufferObject.onUpdateVertices(this);
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public void invalidateText() {
        this.setText(this.mText);
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}